<!--
    by: yuanbiao 2025年06月07日
    name: x-curved-text
    notes:
-->
<template>
  <svg :class="[prefixCls]" :viewBox="`0 0 ${width} ${height}`" width="100%" height="100%">
    <g>
      <path :id="id" :class="`${prefixCls}__path`"
        :d="`M${xPoints.x0},${xPoints.y0} Q${xPoints.x1},${xPoints.y1} ${xPoints.x2},${xPoints.y2}`" />
      <text :class="[`${prefixCls}__text`, {[`${prefixCls}__text_ban`]: pointActiveIndex}]"
        dominant-baseline="middle" text-anchor="start" @click.stop="$emit('clickText')">
        <textPath :href="`#${id}`" startOffset="0%" method="align" spacing="auto" lengthAdjust="spacingAndGlyphs">
          <tspan v-for="(v,i) in wordList" :key="i" :data-i="i" :class="`${prefixCls}__word`"
            :style="v.style || {}" :dy="v.dy || 0" @click="$emit('clickWord',v,i)"
          >
            <template v-if="(v.letters || []).length">
              <tspan v-for="(v1,i1) in (v.letters || [])" :key="`${i}_${i1}`" :data-i="i1" :style="v1.style || {}"
                :class="`${prefixCls}__letter`" @click="$emit('clickLetter',v1,i1)"
              >{{ v1.text }}</tspan>
            </template>
            <template v-else>
              {{ v.text }}
            </template>
            <tspan :style="{'font-size': v.wordSpacing || 'inherit'}">&emsp;</tspan>
          </tspan>
        </textPath>
      </text>
      <template v-if="controllable">
        <!-- 起点控制点 -->
        <circle ref="startPoint" :class="`${prefixCls}__control_point`"
          :cx="xPoints.x0" :cy="xPoints.y0" r="6"
          @touchstart.stop="touchstartHandler($event,'0')"
          @mousedown.stop="mousedownHandler($event,'0')"
        />
        <!-- 控制点 -->
        <circle ref="controlPoint" :class="`${prefixCls}__control_point`"
          :cx="xPoints.x1" :cy="xPoints.y1" r="6"
          @touchstart.stop="touchstartHandler($event,'1')"
          @mousedown.stop="mousedownHandler($event,'1')"
        />
        <!-- 终点控制点 -->
        <circle ref="endPoint" :class="`${prefixCls}__control_point`"
          :cx="xPoints.x2" :cy="xPoints.y2" r="6"
          @touchstart.stop="touchstartHandler($event,'2')"
          @mousedown.stop="mousedownHandler($event,'2')"
        />
        <!-- 控制线 -->
        <line :class="`${prefixCls}__control_line`"
        :x1="xPoints.x0" :y1="xPoints.y0" :x2="xPoints.x1" :y2="xPoints.y1" />
        <line :class="`${prefixCls}__control_line`"
        :x1="xPoints.x1" :y1="xPoints.y1" :x2="xPoints.x2" :y2="xPoints.y2" />
      </template>
    </g>
  </svg>
</template>

<script>
const prefixCls = 'x-curved-text'
let rawPoints = {
  x0: 100, y0: 300, 'x0%': 20, 'y0%': 60,
  x1: 200, y1: 200, 'x1%': 40, 'y1%': 40,
  x2: 300, y2: 100, 'x2%': 60, 'y2%': 20,
  unit: 'px'
}

export default {
  name: 'x-curved-text',
  props: {
    width: { type: Number, default: 500 },
    height: { type: Number, default: 500 },
    points: { type: Object, default () {
      return {...rawPoints}
    } },
    wordList: { type: Array, default () {
      return [
        // {
        //   text: 'hello',
        //   dy: '-0.5em',
        //   wordSpacing: 10,
        //   style: {
        //     fill: 'red', fontSize: 20, fontFamily: 'Arial', fontWeight: 'bold', fontStyle: 'italic',
        //   },
        //   letters: [
        //     {
        //       text: 'hello',
        //       style: {
        //         fill: 'green', fontSize: 20, fontFamily: 'Arial', fontWeight: 'bold', fontStyle: 'italic',
        //       }
        //     },
        //   ]
        // }
      ]
    } },
    controllable: { type: Boolean, default: true },
  },
  data() {
    return {
      prefixCls: prefixCls,
      id: `xPath_${Math.random().toString().substring(2)}`,
      xPoints: {...rawPoints},
      pointActiveIndex: '',
      dragInfo: { x_: 0, y_: 0, x0: 0, y0: 0, x1: 0, y1: 0 },
    }
  },
  computed: {
    mark () {
      return this.getMark(this.xPoints)
    }
  },
  watch: {
    points: {
      handler (n) {
        let _mark = this.getMark(n)
        if (_mark !== this.mark) {
          this.mergePoints(n)
        }
      },
      immediate: true,
      deep: true
    },
    mark (n, o) {
      if (n !== o) {
        this.$emit('change', this.getPointInfo())
      }
    }
  },
  methods: {
    mergePoints (points) {
      let xPoints = {...(points || {})}
      xPoints.x0 = parseFloat(xPoints.x0) || 0
      xPoints.y0 = parseFloat(xPoints.y0) || 0
      xPoints.x1 = parseFloat(xPoints.x1) || 0
      xPoints.y1 = parseFloat(xPoints.y1) || 0
      xPoints.x2 = parseFloat(xPoints.x2) || 0
      xPoints.y2 = parseFloat(xPoints.y2) || 0
      if (xPoints.unit === '%') {
        xPoints['x0%'] = xPoints.x0
        xPoints['y0%'] = xPoints.y0
        xPoints['x1%'] = xPoints.x1
        xPoints['y1%'] = xPoints.y1
        xPoints['x2%'] = xPoints.x2
        xPoints['y2%'] = xPoints.y2
        xPoints.x0 = this.per2px(xPoints['x0'], this.width)
        xPoints.y0 = this.per2px(xPoints['y0'], this.height)
        xPoints.x1 = this.per2px(xPoints['x1'], this.width)
        xPoints.y1 = this.per2px(xPoints['y1'], this.height)
        xPoints.x2 = this.per2px(xPoints['x2'], this.width)
        xPoints.y2 = this.per2px(xPoints['y2'], this.height)
      } else {
        xPoints['x0%'] = this.px2per(xPoints.x0, this.width)
        xPoints['y0%'] = this.px2per(xPoints.y0, this.height)
        xPoints['x1%'] = this.px2per(xPoints.x1, this.width)
        xPoints['y1%'] = this.px2per(xPoints.y1, this.height)
        xPoints['x2%'] = this.px2per(xPoints.x2, this.width)
        xPoints['y2%'] = this.px2per(xPoints.y2, this.height)
      }
      this.xPoints = {...this.xPoints, ...xPoints}
    },
    // 开始拖动
    touchstartHandler (e, pointIndex) {
      this.pointActiveIndex = pointIndex
      this.dragInfo.x1 = this.dragInfo.x0 = e.touches[0].clientX
      this.dragInfo.y1 = this.dragInfo.y0 = e.touches[0].clientY
      this.operationBefore()
      window.addEventListener('touchmove', this.touchmoveHandler)
      window.addEventListener('touchend', this.touchendHandler)
    },
    mousedownHandler (e, pointIndex) {
      this.pointActiveIndex = pointIndex
      this.dragInfo.x1 = this.dragInfo.x0 = e.clientX
      this.dragInfo.y1 = this.dragInfo.y0 = e.clientY
      this.operationBefore()
      window.addEventListener('mousemove', this.mousemoveHandler)
      window.addEventListener('mouseup', this.mouseupHandler)
      window.addEventListener('mouseleave', this.mouseupHandler)
    },
    // 拖动过程
    touchmoveHandler (e) {
      if (this.pointActiveIndex && e.touches[0]) {
        this.dragInfo.x1 = e.touches[0].clientX
        this.dragInfo.y1 = e.touches[0].clientY
        this.operationProcess()
      }
    },
    mousemoveHandler (e) {
      if (this.pointActiveIndex) {
        this.dragInfo.x1 = e.clientX
        this.dragInfo.y1 = e.clientY
        this.operationProcess()
      }
    },
    // 停止拖动
    touchendHandler () {
      this.operationAfter()
      this.pointActiveIndex = ''
      window.removeEventListener('touchmove', this.touchmoveHandler)
      window.removeEventListener('touchend', this.touchendHandler)
    },
    mouseupHandler () {
      this.operationAfter()
      this.pointActiveIndex = ''
      window.removeEventListener('mousemove', this.mousemoveHandler)
      window.removeEventListener('mouseup', this.mouseupHandler)
      window.removeEventListener('mouseleave', this.mouseupHandler)
    },
    operationBefore () {
      this.dragInfo.x_ = this.xPoints[`x${this.pointActiveIndex}`]
      this.dragInfo.y_ = this.xPoints[`y${this.pointActiveIndex}`]
    },
    operationProcess () {
      let dx = this.dragInfo.x1 - this.dragInfo.x0
      let dy = this.dragInfo.y1 - this.dragInfo.y0
      let vx = this.dragInfo.x_ + dx
      let vy = this.dragInfo.y_ + dy
      this.$set(this.xPoints, `x${this.pointActiveIndex}`, vx)
      this.$set(this.xPoints, `y${this.pointActiveIndex}`, vy)
      this.$set(this.xPoints, `x${this.pointActiveIndex}%`, this.px2per(vx, this.width))
      this.$set(this.xPoints, `y${this.pointActiveIndex}%`, this.px2per(vy, this.height))
    },
    operationAfter () {
      if (this.pointActiveIndex) {
        let dx = this.dragInfo.x1 - this.dragInfo.x0
        let dy = this.dragInfo.y1 - this.dragInfo.y0
        let d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
        if (d < 5) {
          this.$emit('tap', this.getPointInfo())
        }
      }
    },
    getPointInfo () {
      let xPoints = { ...this.xPoints }
      // 修正因width和height变化导致百分比变化
      if (this.xPoints.unit === '%') {
        xPoints.x0 = this.per2px(xPoints['x0%'], this.width)
        xPoints.y0 = this.per2px(xPoints['y0%'], this.height)
        xPoints.x1 = this.per2px(xPoints['x1%'], this.width)
        xPoints.y1 = this.per2px(xPoints['y1%'], this.height)
        xPoints.x2 = this.per2px(xPoints['x2%'], this.width)
        xPoints.y2 = this.per2px(xPoints['y2%'], this.height)
      } else {
        xPoints['x0%'] = this.px2per(xPoints.x0, this.width)
        xPoints['y0%'] = this.px2per(xPoints.y0, this.height)
        xPoints['x1%'] = this.px2per(xPoints.x1, this.width)
        xPoints['y1%'] = this.px2per(xPoints.y1, this.height)
        xPoints['x2%'] = this.px2per(xPoints.x2, this.width)
        xPoints['y2%'] = this.px2per(xPoints.y2, this.height)
      }
      this.xPoints = xPoints
      return { width: this.width, height: this.height, ...xPoints }
    },
    getMark (data) {
      data = data || {}
      let unit = this.points.unit || 'px'
      let result = [
        data['x0'], data['y0'],
        data['x1'], data['y1'],
        data['x2'], data['y2']
      ].join('_')
      if (unit === '%' && Object.prototype.hasOwnProperty.call(data, 'x0%')) {
        result = [
          this.width, this.height,
          data['x0%'], data['y0%'],
          data['x1%'], data['y1%'],
          data['x2%'], data['y2%']
        ].join('_')
      }
      return result
    },
    px2per (px, total) {
      let result = 0
      px = parseFloat(px)
      if (!isNaN(px)) {
        result = ((px / total || 0) * 100).toFixed(1)
      }
      return parseFloat(result)
    },
    per2px (per, total) {
      per = parseFloat(per) || 0
      total = parseFloat(total) || 0
      return total * per / 100
    },
  }
}
</script>
